package com.jddup.program.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.stream.Collectors;

/**
 * Created by ZZB on 2017/12/4.
 */
public class ClasspathPackageScanner {
    private Logger logger = LoggerFactory.getLogger(ClasspathPackageScanner.class);

    private String basePackage;
    private ClassLoader cl;

    /**
     * Construct an instance and specify the base package it should scan.
     *
     * @param basePackage The base package to scan.
     */
    public ClasspathPackageScanner(String basePackage) {
        this.basePackage = basePackage;
        this.cl = getClass().getClassLoader();

    }

    /**
     * Construct an instance with base package and class loader.
     *
     * @param basePackage The base package to scan.
     * @param cl          Use this class load to locate the package.
     */
    public ClasspathPackageScanner(String basePackage, ClassLoader cl) {
        this.basePackage = basePackage;
        this.cl = cl;
    }

    /**
     * Get all fully qualified names located in the specified package
     * and its sub-package.
     *
     * @return A list of fully qualified names.
     * @throws IOException
     */
    public List<String> getFullyQualifiedClassNameList() throws IOException {
        logger.info("开始扫描包{}下的所有类", basePackage);

        return doScan(basePackage, new ArrayList<>());
    }

    public List<Class> getClassList() {
        try {
            List<String> classNames = doScan(basePackage, new ArrayList<>());
            return classNames.stream().map(s -> {
                try {
                    return Class.forName(s);
                } catch (NoClassDefFoundError | ClassNotFoundException e) {
                    logger.error("通过classname载入class [{}]异常", s);
                    return null;
                }
            }).filter(Objects::nonNull).collect(Collectors.toList());
        } catch (IOException e) {
            logger.error("扫描包[{}]失败", basePackage);
            return null;
        }
    }


    /**
     * Actually perform the scanning procedure.
     *
     * @param basePackage
     * @param nameList    A list to contain the result.
     * @return A list of fully qualified names.
     * @throws IOException
     */
    private List<String> doScan(String basePackage, List<String> nameList) throws IOException {
        // replace dots with splashes
        String splashPath = dotToSplash(basePackage);

        List<String> names = new ArrayList<>(); // contains the name of the class file. e.g., Apple.class will be stored as "Apple"
        // get file path
        try {
            Enumeration<URL> urls = cl.getResources(splashPath);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                String filePath = getRootPath(url);

                // Get classes in that package.
                // If the web server unzips the jar file, then the classes will exist in the form of
                // normal file in the directory.
                // If the web server does not unzip the jar file, then classes will exist in jar file.
                if (isJarFile(filePath)) {
                    // jar file
                    if (logger.isDebugEnabled()) {
                        logger.debug("{} 是一个JAR包", filePath);
                    }

                    names.addAll(readFromJarFile(filePath, splashPath));
                } else {
                    // directory
                    if (logger.isDebugEnabled()) {
                        logger.debug("{} 是一个目录", filePath);
                    }

                    names.addAll(readFromDirectory(filePath));
                }
            }

            for (String name : names) {
                if (isClassFile(name)) {
                    //nameList.add(basePackage + "." + StringUtil.trimExtension(name));
                    nameList.add(toFullyQualifiedName(name, basePackage));
                } else {
                    // this is a directory
                    // check this directory for more classes
                    // do recursive invocation
                    doScan(basePackage + "." + name, nameList);
                }
            }

            if (logger.isDebugEnabled()) {
                for (String n : nameList) {
                    logger.debug("找到 {}", n);
                }
            }
        } catch (IOException e) {
            //TODO
        }

        return nameList;
    }

    /**
     * Convert short class name to fully qualified name.
     * e.g., String -> java.lang.String
     */
    private String toFullyQualifiedName(String shortName, String basePackage) {
        StringBuilder sb = new StringBuilder(basePackage);
        sb.append('.');
        sb.append(trimExtension(shortName));

        return sb.toString();
    }

    private List<String> readFromJarFile(String jarPath, String splashedPackageName) throws IOException {
        if (logger.isDebugEnabled()) {
            logger.debug("从JAR包中读取类: {}", jarPath);
        }

        JarInputStream jarIn = new JarInputStream(new FileInputStream(jarPath));
        JarEntry entry = jarIn.getNextJarEntry();

        List<String> nameList = new ArrayList<>();
        while (null != entry) {
            String name = entry.getName();
            if (name.startsWith(splashedPackageName) && isClassFile(name)) {
                String className = name.replace(splashedPackageName + "/", "").replace("/", ".");
                logger.debug("find jar entry name : " + className);
                nameList.add(className);
            }

            entry = jarIn.getNextJarEntry();
        }

        return nameList;
    }

    private List<String> readFromDirectory(String path) {
        File file = new File(path);
        String[] names = file.list();

        if (null == names) {
            return null;
        }

        return Arrays.asList(names);
    }

    private boolean isClassFile(String name) {
        return name.endsWith(".class");
    }

    private boolean isJarFile(String name) {
        return name.endsWith(".jar");
    }

    /**
     * "file:/home/whf/cn/fh" -> "/home/whf/cn/fh"
     * "jar:file:/home/whf/foo.jar!cn/fh" -> "/home/whf/foo.jar"
     */
    public String getRootPath(URL url) {
        String fileUrl = url.getFile();
        int pos = fileUrl.indexOf('!');

        if (-1 == pos) {
            return fileUrl;
        }

        return fileUrl.substring(5, pos);
    }

    /**
     * "cn.fh.lightning" -> "cn/fh/lightning"
     *
     * @param name
     * @return
     */
    public String dotToSplash(String name) {
        return name.replaceAll("\\.", "/");
    }

    /**
     * "Apple.class" -> "Apple"
     */
    public String trimExtension(String name) {

        if (name.endsWith(".class")) {
            return name.substring(0, name.length() - ".class".length());
        }

        return name;
    }

    /**
     * /application/home -> /home
     *
     * @param uri
     * @return
     */
    public String trimURI(String uri) {
        String trimmed = uri.substring(1);
        int splashIndex = trimmed.indexOf('/');

        return trimmed.substring(splashIndex);
    }

    /**
     * For test purpose.
     */
    public static void main(String[] args) throws Exception {
        ClasspathPackageScanner scan = new ClasspathPackageScanner("com.jddup.program");
        List<Class> classList = scan.getClassList();
        classList.forEach(System.out::println);
    }

}
